﻿using System;
using System.Collections.Generic;
using TimbreRecognition.Recognition.Model;
using TimbreRecognition.Util;

namespace TimbreRecognition.Recognition.Teacher
{
    public class BackPropagationTeacher : ITeacher
    {
        private const int MaxRoundCount = 250;

        //rapidity
        private const double N = 0.05;

        //inertness
        private const double A = 0.0;

        private const int PrintInterval = 1;

        private readonly double error;

        public ILogger Logger { get; set; }


        public BackPropagationTeacher(double error)
        {
            this.error = error;
        }

        public void teach(NeuronNetwork network, Dictionary<double[], double[]> data, Dictionary<double[], double[]> validationData)
        {

            NetworkClassifierWrapper networkWrapper = new NetworkClassifierWrapper(network);

            ResultTester tester = new ResultTester();

            int round = 1;
            double errorOnData;
            double minError = Double.MaxValue;

            do
            {
                double totalError = 0;

                data = DataUtil.Randomize(data);

                foreach (KeyValuePair<double[], double[]> entry in data)
                {
                    double[] input = entry.Key;
                    double[] expectedOutput = entry.Value;

                    teach(network, input, expectedOutput);

                    double[] result = network.getOutput();

                    totalError += getNetworkError(result, expectedOutput);
                }


                errorOnData = tester.GetErrorNumber(validationData, networkWrapper);

                double averageError = totalError / data.Count;

                if (averageError < minError)
                {
                    minError = averageError;
                }

                if (round % PrintInterval == 0)
                {
                    Log("Round = " + round + ", v.error = " + errorOnData + ", t.error = " + averageError);
                }

                if (round % MaxRoundCount == 0)
                {
                    Log("Max number of round has been reached");
                    Log("Max accuracy = " + (1 - minError));
                    break;
                }

                round++;

            } while (errorOnData > error);

            Log("Number of rounds = " + round);
        }

        private void teach(NeuronNetwork network, double[] input, double[] expectedOutput)
        {
            network.recalculateOutput(input);
            calculateErrors(network, expectedOutput);
            changeWeight(network, input);
        }

        private void changeWeight(NeuronNetwork network, double[] input)
        {
            List<NeuronLayer> layers = network.getLayers();
            for (int i = 0; i < layers.Count; i++)
            {
                NeuronLayer layer = layers[i];
                List<Neuron> neurons = layer.getNeurons();
                foreach (Neuron neuron in neurons)
                {
                    double[] weights = neuron.getWeights();
                    double[] deltas = new double[weights.Length];
                    double[] previousWeights = neuron.getDeltas();
                    for (int j = 0; j < weights.Length; j++)
                    {
                        if (i != 0)
                        {
                            NeuronLayer relatedLayer = layers[i - 1];
                            List<Neuron> relatedNeurons = relatedLayer.getNeurons();
                            Neuron relatedNeuron = relatedNeurons[j];
                            double delta = -N * neuron.getError() * relatedNeuron.getOutput();
                            weights[j] += delta;
                            deltas[j] = delta;
                        }
                        else
                        {
                            //input layer
                            double delta = -N * neuron.getError() * input[j];
                            weights[j] += delta + A * previousWeights[j];
                            deltas[j] = delta;
                        }
                    }
                    double newBias = neuron.getBias() - neuron.getError();
                    neuron.setBias(newBias);
                    neuron.setDeltas(deltas);
                }
            }
        }

        private void calculateErrors(NeuronNetwork network, double[] expectedOutput)
        {
            List<NeuronLayer> layers = network.getLayers();
            for (int i = layers.Count - 1; i >= 0; i--)
            {
                NeuronLayer layer = layers[i];
                List<Neuron> neurons = layer.getNeurons();
                for (int j = 0; j < neurons.Count; j++)
                {
                    Neuron neuron = neurons[j];
                    double error;
                    if (i != layers.Count - 1)
                    {
                        NeuronLayer relatedLayer = layers[i + 1];
                        List<Neuron> relatedNeurons = relatedLayer.getNeurons();
                        error = getHiddenLayerError(neuron, relatedNeurons, j);
                    }
                    else
                    {
                        //Output layer
                        error = getOutputLayerError(neuron.getOutput(), expectedOutput[j]);
                    }
                    neuron.setError(error);
                }
            }
        }

        private double getNetworkError(double[] output, double[] expectedOutput)
        {
            double error = 0;
            for (int i = 0; i < output.Length; i++)
            {
                error += (output[i] - expectedOutput[i]) * (output[i] - expectedOutput[i]);
            }
            return 0.5 * error;
        }

        private double getOutputLayerError(double output, double expectedOutput)
        {
            return Neuron.A * output * (1 - output) * (output - expectedOutput);
        }

        private double getHiddenLayerError(Neuron neuron, List<Neuron> relatedNeurons, int weightIndex)
        {
            double relatedErrorSum = 0;
            foreach (Neuron relatedNeuron in relatedNeurons)
            {
                double[] weights = relatedNeuron.getWeights();
                relatedErrorSum += relatedNeuron.getError() * weights[weightIndex];
            }
            return Neuron.A * neuron.getOutput() * (1 - neuron.getOutput()) * relatedErrorSum;
        }

        private void Log(string message)
        {
            if (Logger != null)
            {
                Logger.Log(message);
            }
        }
    }
}

